perf: build std string encoders and escapers in linear time - #699
Merged
Conversation
these functions accumulated their output with `s = s + piece` in a loop, so each append recopied the whole string built so far and the work grew quadratically with the input. rebuild each one through a byte buffer or a list join and materialize the string once at the end, the same shape #686 gave base64. the network-facing hot paths are url percent encode/decode (every query param, form body, and csrf token), json string unescaping inside parse, grpc-message percent coding, and template `<% for %>` rendering. the rest are std primitives a lot of higher-level code sits on: hex, base32, and base58 encoding; the log, metrics, and html escapers; strings swap_case, reverse, and repeat; text fold, sanitize, from_chars, from_code_points; regex replace_all; term strip; collections join_with; yaml quoted-scalar decoding; and config json string reading. output is byte-identical: the encoders and escapers are pinned against python's urllib, json, and binascii and the rfc base32/base58 vectors at sizes from 0 to 65537, and a new golden test locks the user-visible ones.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A set of std functions built their output with
s = s + pieceinside a loop. Each append recopies the whole string accumulated so far, so the work grew quadratically with the input. This is the same shape #686 fixed in base64; the worst availability offenders (redis, http2, the io buffered readers) were fixed in #687/#689/#695, and this cleans up the char-by-char remainder. Each site now accumulates through a byte buffer or a list join and materializes the string once at the end.The network-facing hot paths are the ones that matter most: url percent encode/decode (every query param, form body, and CSRF token), json string unescaping inside
parse, grpc-message percent coding, and template<% for %>rendering. The rest are std primitives a lot of higher-level code sits on: hex, base32, and base58 encoding; the log, metrics, and html escapers; stringsswap_case/reverse/repeat; textfold/sanitize/from_chars/from_code_points; regexreplace_all; termstrip; collectionsjoin_with; yaml quoted-scalar decoding; and config json string reading.While surveying I found and fixed several sites beyond the ticket's list, all the same shape: grpc
percent_encode/percent_decode, yamldecode_double_quoted/decode_single_quoted/repeat_newlines, stringsswap_case/reverse/repeat_text, textfrom_chars/from_code_points/sanitize/fold, regexreplace_all, termstrip, collectionsjoin_with, metricsnormalize_metric_name, and configconfig_json_read_string. No non-std code was touched.Two decode paths (url
decode, grpcpercent_decode) accumulate through aList[String]join rather than a byte buffer on purpose: a decoded byte can be invalid UTF-8 (%ff), which a buffer round-trip through UTF-8 validation would reject, so the list preserves the existing behavior exactly.what was tested
Output is byte-identical. For every fixed site I ran a checksum sweep at n = 0, 1, 2, 3, 17, 255, 1000, 4096, 65537 against the old and new std and diffed: identical at every size. The encoders and escapers were also anchored to external oracles, not self-round-tripping:
urllib.parse— matched at every sizeparse, surrogate pairs included) vs pythonjson.loads— matchedbinascii.hexlifyover all 256 byte values — matchedA new golden test,
tests/cases/test_string_builder_linearity.pith, locks the user-visible encoders (url, json, hex, base32, base58, html, template) to their oracle-verified output.The curve flattens. Interleaved A/B wall-time (min of 3 trials) at 4x size steps, so the shape is what to read — quadratic should step ~16x, linear ~4x:
A 200k-iteration leak smoke over the rewritten builders held steady at ~600 MB peak RSS on both old and new std (no per-call leak from the parts lists or buffers).
Suites, one at a time:
run-regressions-only342/0,check-invalid-only44/0 (after rebuildingself-host/pith_main),run-examples-self-only121/121. No doc or example describes these functions' internal accumulation strategy, so nothing there needed updating.